/*
 * Galaxium Messenger
 * Copyright (C) 2005-2007 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;

using Anculus.Core;

using Galaxium.Core;

namespace Galaxium.Protocol.Msn
{
	public class MsnObject
	{
		static string _cacheDir;
		static Dictionary<string, P2PObjectTransfer> _objectTransfers;
		
		public delegate void WinkRequestDelegate ();
		
		byte[] _data = new byte[0];
		MemoryStream _updateStream = new MemoryStream ();
		MsnSession _session;
		XmlElement _xml;
		
		public event EventHandler DataChanged;

		public MsnSession Session
		{
			get { return _session; }
		}
		
		protected XmlElement Xml
		{
			get { return _xml; }
		}
		
		public virtual string Context
		{
			get
			{
				string context = _xml.OuterXml;
				
				// aMSN doesn't recognise the object if we include the space...
				// (Gives error "Couln't find filename for context")
				if (context.EndsWith (" />"))
					context = context.Substring (0, context.Length - 3) + "/>";
				
				return context;
			}
			set
			{
				try
				{
					ThrowUtility.ThrowIfEmpty ("context", value);
				
					// Decode context if needed
					string context = EncodingUtility.UrlDecodeSafe (value).Trim ();
					
					if (context == "0")
					{
						SetDefaults ();
						return;
					}
					
					// Ugly workaround for kmess which doesn't close the xml tag
					if (context.StartsWith ("<msnobj", StringComparison.InvariantCultureIgnoreCase) && (!context.EndsWith ("/>")))
					{
						if (context.EndsWith ("/"))
							context += ">";
						else
							context += "/>";
					}
				
					XmlDocument xmlDoc = new XmlDocument ();
					xmlDoc.LoadXml (context);
				
					ThrowUtility.ThrowIfFalse ("No msnobject XML DocumentElement", xmlDoc.DocumentElement.Name.Equals ("msnobj", StringComparison.InvariantCultureIgnoreCase));
				
					_xml = xmlDoc.DocumentElement;
				
					LoadCache ();
				}
				catch (Exception ex)
				{
					Log.Warn (ex, "Invalid MsnObject Context {0}", value);
					SetDefaults ();
				}
			}
		}
		
		public string ContextEncoded
		{
			get { return EncodingUtility.UrlEncode (Context); }
		}
		
		public byte[] Data
		{
			get { return _data; }
			set
			{
				_data = value;
				_xml.SetAttribute ("Size", _data.Length.ToString ());
				
				UpdateHash ();
				CacheData ();
				OnDataChanged ();
			}
		}
		
		public string Checksum
		{
			get { return _xml.GetAttribute ("SHA1C"); }
		}

		public IMsnEntity Creator
		{
			get
			{
				if (string.IsNullOrEmpty (_xml.GetAttribute ("Creator")))
					return null;
				
				return _session.FindEntity (_xml.GetAttribute ("Creator")) as IMsnEntity;
			}
			set
			{
				_xml.SetAttribute ("Creator", value.UniqueIdentifier);
				UpdateChecksum ();
			}
		}
		
		public int Size
		{
			get
			{
				if (string.IsNullOrEmpty (_xml.GetAttribute ("Size")))
					return 0;
				
				return int.Parse (_xml.GetAttribute ("Size"));
			}
		}
		
		public MsnObjectType Type
		{
			get
			{
				if (string.IsNullOrEmpty (_xml.GetAttribute ("Type")))
					return MsnObjectType.Unknown;
				
				return (MsnObjectType)int.Parse (_xml.GetAttribute ("Type"));
			}
			set
			{
				if (Type != value)
				{
					_xml.SetAttribute ("Type", ((int)value).ToString ());
					UpdateChecksum ();
				}
			}
		}
		
		public string Location
		{
			get { return _xml.GetAttribute ("Location"); }
			set
			{
				_xml.SetAttribute ("Location", value);
				UpdateChecksum ();
			}
		}
		
		public string Friendly
		{
			get
			{
				string str = Encoding.Unicode.GetString (Convert.FromBase64String (_xml.GetAttribute("Friendly")));
				
				if (str.Contains ("\0"))
					str = str.Substring (0, str.IndexOf ("\0"));
				
				return str;
			}
			set
			{
				_xml.SetAttribute("Friendly", Convert.ToBase64String (Encoding.Unicode.GetBytes (value + "\0")));
				UpdateChecksum ();
			}
		}
		
		public string Sha
		{
			get	{ return _xml.GetAttribute("SHA1D"); }
			internal set
			{
				_xml.SetAttribute ("SHA1D", value);
				UpdateChecksum ();
			}
		}
		
		public Stream UpdateStream
		{
			get { return _updateStream; }
		}
		
		public string CacheFilename
		{
			get
			{
				if (_session == null)
					return null;
				
				return Path.Combine (GetCacheDirectory (Creator, Type), EncodingUtility.Base64Encode (Sha));
			}
		}
		
		static MsnObject ()
		{
			_cacheDir = Path.Combine (Path.Combine (CoreUtility.GetConfigurationSubDirectory ("Cache"), "MSN"), "ObjectData");
			BaseUtility.CreateDirectoryIfNeeded (_cacheDir);
			
			_objectTransfers = new Dictionary<string, P2PObjectTransfer> ();
		}
		
		public static string GetCacheDirectory (IMsnEntity entity, MsnObjectType type)
		{
			string dir = Path.Combine (Path.Combine (_cacheDir, entity.UniqueIdentifier), type.ToString ());
			
			BaseUtility.CreateDirectoryIfNeeded (dir);
			
			return dir;
		}
		
		protected MsnObject (MsnSession session, string context)
		{
			ThrowUtility.ThrowIfNull ("session", session);
			ThrowUtility.ThrowIfEmpty ("context", context);
			
			_session = session;
			Context = context;
		}
		
		public MsnObject (MsnSession session)
		{
			ThrowUtility.ThrowIfNull ("session", session);
		
			_session = session;
			
			SetDefaults ();
		}
		
		void SetDefaults ()
		{
			XmlDocument xmlDoc = new XmlDocument ();
			_xml = xmlDoc.CreateElement ("msnobj");
			xmlDoc.AppendChild (_xml);
			
			if (_session != null)
				Creator = _session.Account as IMsnEntity;
			
			Friendly = string.Empty;
			Location = "0";
			Type = MsnObjectType.Unknown;
			_xml.SetAttribute ("Size", "0");
		}
		
		protected virtual void UpdateHash ()
		{
			_xml.SetAttribute ("SHA1D", CalculateHash ());
			UpdateChecksum ();
		}
		
		protected virtual string CalculateHash ()
		{
			HashAlgorithm hash = new SHA1Managed ();			
			byte[] hashBytes = hash.ComputeHash (_data);
			return Convert.ToBase64String (hashBytes);
		}
		
		protected virtual void UpdateChecksum ()
		{
			_xml.SetAttribute ("SHA1C", CalculateChecksum ());
		}
		
		protected virtual string CalculateChecksum ()
		{
			string checksum = string.Format ("Creator{0}Size{1}Type{2}Location{3}Friendly{4}SHA1D{5}",
			                                 _xml.GetAttribute ("Creator"),
			                                 _xml.GetAttribute ("Size"),
			                                 _xml.GetAttribute ("Type"),
			                                 _xml.GetAttribute ("Location"),
			                                 _xml.GetAttribute ("Friendly"),
			                                 _xml.GetAttribute ("SHA1D"));

			HashAlgorithm shaAlg = new SHA1Managed ();
			return Convert.ToBase64String (shaAlg.ComputeHash (Encoding.ASCII.GetBytes (checksum)));
		}
		
		public virtual void UpdateFromStream ()
		{
			byte[] newData = new byte[_updateStream.Length];
			_updateStream.Seek (0, SeekOrigin.Begin);
			_updateStream.Read (newData, 0, (int)_updateStream.Length);
			
			Data = newData;
		}
		
		protected virtual void OnDataChanged ()
		{
			if (DataChanged != null)
				DataChanged (this, EventArgs.Empty);
		}
		
		protected virtual void CacheData ()
		{
			File.WriteAllBytes (CacheFilename, _data);
		}
		
		protected virtual void LoadCache ()
		{
			if ((!string.IsNullOrEmpty (CacheFilename)) && File.Exists (CacheFilename))
			{
				_data = File.ReadAllBytes (CacheFilename);
				OnDataChanged ();
			}
			else
				_data = new byte[0];
		}
		
		public void Request (WinkRequestDelegate callback)
		{
			if (_data.Length > 0)
			{
				if (callback != null)
					callback ();
				
				return;
			}
			
			if (string.IsNullOrEmpty (Sha))
			{
				Log.Debug ("Not requesting MsnObject with empty Sha: {0}", Context);
				return;
			}
			
			if (!(Creator is MsnContact))
			{
				Log.Debug ("Not requesting MsnObject with non-MsnContact creator: {0}", Context);
				return;
			}
			
			// We can't request objects from offline contacts or those on other networks (eg. yahoo)
			if ((Creator.Presence == MsnPresence.Offline) || (Creator.Network != Network.WindowsLive))
			{
				//Log.Debug ("Not requesting MsnObject with offline or non-msn creator: {0}", Context);
				return;
			}
			
			// We're already transferring the object
			if (_objectTransfers.ContainsKey (Sha))
			{
				// Once the transfer is finished the data should be cached & we can load it
				_objectTransfers[Sha].Complete += delegate
				{
					LoadCache ();
					
					if (callback != null)
						callback ();
				};
				
				return;
			}
			
			if (_data.Length == 0)
			{
				// See if we have the data cached
				LoadCache ();
				
				// We don't, request it
				if (_data.Length == 0)
				{
					P2PObjectTransfer transfer = new P2PObjectTransfer (this);
					
					_objectTransfers.Add (Sha, transfer);
					MsnP2PUtility.Add (transfer);
					
					transfer.Complete += delegate
					{
						_objectTransfers.Remove (Sha);
						
						if (callback != null)
							callback ();
					};
				}
				else if (callback != null)
					callback ();
			}
		}
		
		public void Request ()
		{
			Request (null);
		}
		
		public static MsnObject Load (MsnSession session, string context)
		{
			if (string.IsNullOrEmpty (context) || (context == "0"))
				return null;
			
			MsnObject tmp = new MsnObject (session, context);
			
			switch (tmp.Type)
			{
			case MsnObjectType.UserDisplay:
				return new MsnDisplayImage (session, context);
			case MsnObjectType.DynamicDisplay:
				return new MsnDynamicDisplayPicture (session, context);
			case MsnObjectType.Emoticon:
				return new MsnEmoticon (session, context);
			case MsnObjectType.Wink:
				return new MsnWink (session, context);
			case MsnObjectType.VoiceClip:
				return new MsnVoiceClip (session, context);
			default:
				return tmp;
			}
		}
	}
}